# -*- coding: utf-8 -*-
# @Time    : 2024/7/24 15:54
# @Author  : yujiahao
# @File    : 60_fastapi_oauth2_scope.py
# @description:OAuth2 作用域


"""
OAuth2 作用域

1. FastAPI 无缝集成 OAuth2 作用域（Scopes），可以直接使用。

2. 作用域是更精密的权限系统，遵循 OAuth2 标准，与 OpenAPI 应用（和 API 自动文档）集成。

3. OAuth2 也是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制。
   - 这些身份验证应用在用户登录应用时使用 OAuth2 提供指定权限。
   - 脸书、谷歌、GitHub、微软、推特就是 OAuth2 作用域登录。

4. 本章介绍如何在 FastAPI 应用中使用 OAuth2 作用域管理验证与授权。

警告

1. 本章内容较难，刚接触 FastAPI 的新手可以跳过。

2. OAuth2 作用域不是必需的，没有它，您也可以处理身份验证与授权。

3. 但 OAuth2 作用域与 API（通过 OpenAPI）及 API 文档集成地更好。

4. 不管怎么说，FastAPI 支持在代码中使用作用域或其它安全/授权需求项。

5. 很多情况下，OAuth2 作用域就像一把牛刀。


OAuth2 作用域与 OpenAPI

1. OAuth2 规范的作用域是由空格分割的字符串组成的列表。

2. 这些字符串支持任何格式，但不能包含空格。

3. 作用域表示的是权限。

4. OpenAPI 中（例如 API 文档）可以定义安全方案。

5. 这些安全方案在使用 OAuth2 时，还可以声明和使用作用域。

6. 作用域只是（不带空格的）字符串。


常用于声明特定安全权限，例如：

1. 常见用例为，users:read 或 users:write

2. 脸书和 Instagram 使用 instagram_basic

3. 谷歌使用 https://www.googleapis.com/auth/drive

说明

1. OAuth2 中，作用域只是声明特定权限的字符串。

2. 是否使用冒号 : 等符号，或是不是 URL 并不重要。

3. 这些细节只是特定的实现方式。

4. 对 OAuth2 来说，它们都只是字符串而已。
"""

from datetime import datetime, timedelta, timezone
from typing import List, Union

import jwt
from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import (
    OAuth2PasswordBearer,
    OAuth2PasswordRequestForm,
    SecurityScopes,
)
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from pydantic import BaseModel, ValidationError

"""
这段代码实现了一个使用 OAuth2 作用域的 FastAPI 应用。它展示了如何通过 OAuth2 作用域来管理验证与授权。
以下是对代码的整理流程和每个接口的详细解释：

整体流程
    - 定义模型：定义了用于表示访问令牌、用户信息、数据库用户信息等的 Pydantic 模型。
    - 密码管理：定义了用于密码哈希和验证的函数。
    - 用户管理：定义了从数据库获取用户和认证用户的函数。
    - JWT 令牌管理：定义了创建 JWT 令牌的函数。
    - 依赖项管理：定义了获取当前用户和当前活跃用户的依赖项函数。
    - 路径操作：定义了各种 API 端点，包括登录、获取当前用户信息、获取用户项目和系统状态。
"""

# 秘钥，用于签名 JWT
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
# JWT 的签名算法
ALGORITHM = "HS256"
# 访问令牌的过期时间（分钟）
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 假用户数据库
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Chains",
        "email": "alicechains@example.com",
        "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm",
        "disabled": True,
    },
}


# 定义 Token 模型 用于表示访问令牌的结构


class Token(BaseModel):
    """这个模型通常用于在用户登录成功后返回给客户端的响应中。客户端会使用这个令牌来进行后续的 API 请求。"""

    # 这是实际的 JWT 令牌字符串，用于认证请求。
    access_token: str
    # 这是令牌的类型，通常是 "bearer"。Bearer 令牌是一种常见的 OAuth 2.0 认证机制，表示客户端必须在请求头中带上这个令牌来访问受保护的资源。
    token_type: str


# 定义 Token 数据模型  用于表示从 JWT 令牌中解码出来的数据。
class TokenData(BaseModel):
    """这个模型通常用于在服务器端解码和验证 JWT 令牌时使用，以便提取出用户信息和权限信息。"""

    # 这是从令牌中提取的用户名，可以为空（None）。
    username: Union[str, None] = None
    # 这是从令牌中提取的作用域列表，表示用户的权限。
    scopes: List[str] = []


# 定义用户模型
class User(BaseModel):
    username: str
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None


# 定义数据库中的用户模型
class UserInDB(User):
    hashed_password: str


# 密码加密上下文

"""
使用 CryptContext 进行密码加密，并配置 OAuth2 密码流以支持作用域。
    - CryptContext：这是 passlib 库中的一个类，用于管理密码哈希方案。
    - schemes=["bcrypt"]：指定使用 bcrypt 作为密码哈希算法。bcrypt 是一种安全的、广泛使用的密码哈希算法。
    - deprecated="auto"：自动处理过时的哈希方案。如果将来需要更改哈希算法，CryptContext 会自动处理旧的哈希方案并将其更新为新的方案。
"""
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# todo 相对之前的jwt 这里的OAuth2安全方案
# 第一个修改的是两个作用域me和item声明 OAth2安全方案
# scopes 参数接收字典，键是作用域、值是作用域的描述

"""
OAuth2PasswordBearer：这是 FastAPI 提供的一个类，用于实现 OAuth2 密码流（Password Flow）。
tokenUrl="token"：指定获取令牌的 URL 路径。在这个例子中，客户端需要向 /token 路径发送请求以获取访问令牌。

scopes：这是一个字典，用于定义不同的作用域及其描述。作用域是 OAuth2 中用于定义权限的字符串。

    - "me"：作用域的名称，表示读取当前用户信息的权限。
    - "Read information about the current user."：作用域的描述。
    - "items"：作用域的名称，表示读取项目的权限。
    - "Read items."：作用域的描述。
"""
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={"me": "Read information about the current user.", "items": "Read items."},
)

# 创建 FastAPI 应用
app = FastAPI()


# 验证密码
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


# 获取密码哈希值
def get_password_hash(password):
    return pwd_context.hash(password)


# 从数据库中获取用户
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 认证用户
def authenticate_user(fake_db, username: str, password: str):
    """
    authenticate_user(fake_db, username: str, password: str)：定义了一个名为 authenticate_user 的函数，接受三个参数：
    fake_db：这是一个数据库连接或会话对象，用于与数据库交互。
    username：这是要认证的用户名。
    password：这是用户输入的密码。
    """
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


# 创建访问令牌

# data: dict：这是一个包含用户信息的字典，通常包括用户ID或其他必要的身份验证信息。
# expires_delta: Union[timedelta, None] = None：这是一个可选参数，表示访问令牌的过期时间。如果未提供，则默认过期时间为15分钟。
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
    """
    代码整体解释
        - 函数定义：定义一个 create_access_token 函数，接受用户数据和可选的过期时间参数。
        - 复制数据：创建一个数据副本以避免修改原始数据。
        - 处理过期时间：检查是否提供了过期时间参数，如果提供，则使用该参数计算过期时间；否则，默认过期时间为15分钟。
        - 更新数据字典：在数据字典中添加 exp 键，值为计算出的过期时间。
        - 编码 JWT：使用密钥和算法将数据字典编码为 JWT。
        - 返回 JWT：返回编码后的 JWT 字符串。
    """
    # 创建一个副本 data，以免修改原始数据。
    to_encode = data.copy()

    # 处理过期时间
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)

    # 在 to_encode 字典中添加一个 exp 键，值为计算出的过期时间。这是 JWT (JSON Web Token) 的标准字段，用于指示令牌的过期时间。
    to_encode.update({"exp": expire})
    # encoded_jwt：使用 jwt.encode 函数将 to_encode 字典编码为一个 JWT。
    # to_encode：要编码的数据字典。
    # SECRET_KEY：用于签名 JWT 的密钥。确保这个密钥足够复杂且安全。
    # ALGORITHM：用于编码的算法，通常是 "HS256" 等。
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    # 返回编码后的 JWT
    return encoded_jwt


# 获取当前用户
#  这段代码实现了一个依赖项函数 get_current_user，用于在 FastAPI 应用中获取当前经过认证的用户，并验证该用户是否具有所需的作用域（权限）

async def get_current_user(
        # todo 1 定义依赖项
        # security_scopes: SecurityScopes：这是一个特殊的参数，用于获取依赖项的作用域（权限）。
        # token: str = Depends(oauth2_scheme)：这是从请求中提取的 OAuth2 令牌。

        security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)
):
    """
        技术细节

    1. Security 是 Depends 的子类：
       - Security 实际上是 Depends 的子类，而且只比 Depends 多一个参数。

    2. 使用 Security 代替 Depends 的好处：
       - FastAPI 可以声明安全作用域，并在内部使用这些作用域。
       - 同时，使用 OpenAPI 存档 API。

    3. 从 fastapi 导入的对象：
       - 从 fastapi 导入的 Query、Path、Depends、Security 等对象，只是返回特殊类的函数。

    使用 SecurityScopes

    1. 修改依赖项 get_current_user：
       - 这是上面的依赖项使用的依赖项。

    2. 声明依赖项：
       - 使用之前创建的 OAuth2 方案，并把它声明为依赖项：oauth2_scheme。
       - 该依赖项函数本身不需要作用域，因此，可以使用 Depends 和 oauth2_scheme。
       - 不需要指定安全作用域时，不必使用 Security。

    3. 声明特殊参数：
       - 声明从 fastapi.security 导入的 SecurityScopes 类型的特殊参数。
       - SecurityScopes 类与 Request 类似（Request 用于直接提取请求对象）。

    """

    # todo 2、定义认证值：
    """    
    # 如果 security_scopes.scopes 不为空，则将其转换为一个用空格分割的字符串，并将其包含在 authenticate_value 中。
    # 如果 security_scopes.scopes 为空，则 authenticate_value 仅为 "Bearer"。
    """

    # 如果请求中包含作用域，则设置认证值
    if security_scopes.scopes:
        """        
        # scopes 是作用域列表，所有依赖项都把它作为子依赖项。也就是说所有依赖
        # 类 SecurityScopes 的）security_scopes 对象还提供了单字符串类型的属性 scope_str，该属性是（要在本例中使用的）用空格分割的作用域。
        """
        authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
    else:

        # 定义认证失败时的异常
        authenticate_value = "Bearer"
        # todo 3、定义异常：
        # credentials_exception：这是一个 HTTP 异常，用于在认证失败时返回 401 状态码
        # 该异常包含了作用域所需的（如有），以空格分割的字符串（使用 scope_str）。
        # 该字符串要放到包含作用域的 WWW-Authenticate 请求头中（这也是规范的要求）。
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": authenticate_value},
    )

    """
    校验 username 与数据形状

    1. 校验是否获取了 username，并抽取作用域：
       - 我们可以校验是否获取了 username，并抽取作用域。

    2. 使用 Pydantic 模型校验数据：
       - 捕获 ValidationError 异常。
       - 如果读取 JWT 令牌或使用 Pydantic 模型验证数据时出错，就会触发之前创建的 HTTPException 异常。

    3. 更新 Pydantic 模型 TokenData：
       - 使用新的属性 scopes 更新 Pydantic 模型 TokenData。

    4. 使用 Pydantic 验证数据：
       - 确保数据中含有由作用域组成的字符串列表，以及 username 字符串等内容。
       - 反之，如果使用字典或其它数据结构，就有可能在后面某些位置破坏应用，形成安全隐患。

    5. 使用用户名验证用户：
       - 还可以使用用户名验证用户，如果没有用户，也会触发之前创建的异常。
    """

    # todo 4、解码和验证令牌：
    # 尝试解码 JWT 令牌，并从中提取用户名 (sub) 和作用域 (scopes)。
    # 如果解码失败或验证失败，则引发 credentials_exception。
    try:
        # 解码JWT令牌
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        # 从令牌中提取用户名
        username: str = payload.get("sub")
        if username is None:
            # 如果用户名不存在，抛出异常
            raise credentials_exception
        # 从令牌中提取作用域
        token_scopes = payload.get("scopes", [])
        # 创建TokenData 对象
        token_data = TokenData(scopes=token_scopes, username=username)
    except (InvalidTokenError, ValidationError):
        # 如果解码或者验证失败，抛出异常
        raise credentials_exception
    # 从数据库中获取用户
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        # 如果用户不存在，抛出异常
        raise credentials_exception

    # 检查请求的作用域是否都包含在令牌的作用域中
    """
    校验 scopes

    1. 校验所有依赖项和依赖要素（包括路径操作）所需的作用域：
       - 这些作用域包含在令牌的 scopes 里，如果不在其中就会触发 HTTPException 异常。

    2. 使用 security_scopes.scopes：
       - 包含所有作用域字符串列表的 security_scopes.scopes。
    """
    for scope in security_scopes.scopes:
        if scope not in token_data.scopes:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Not enough permissions",
                headers={"WWW-Authenticate": authenticate_value},
            )
    # 返回用户对象
    return user


# 获取当前活跃用户
# todo 在路径操作与依赖项中声明作用域
async def get_current_active_user(
        current_user: User = Security(get_current_user, scopes=["me"]),
):
    """
    为路径操作 /users/me/items/ 声明作用域 items

    1. 从 fastapi 中导入并使用 Security：
       - Security 声明依赖项的方式和 Depends 一样。
       - 但 Security 还能接收作用域（字符串）列表类型的参数 scopes。

    2. 使用 Security 声明依赖项：
       - 使用与 Depends 相同的方式，把依赖项函数 get_current_active_user 传递给 Security。
       - 同时，还传递了作用域列表，本例中只传递了一个作用域：items（此处支持传递更多作用域）。

    3. 依赖项函数 get_current_active_user 还能声明子依赖项：
       - 不仅可以使用 Depends，也可以使用 Security。
       - 声明子依赖项函数（get_current_user）及更多作用域。

    本例要求使用作用域 me（还可以使用更多作用域）。

    """

    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


# 登录获取访问令牌

# todo JWT 令牌作用域

"""
修改令牌路径操作，返回请求的作用域¶

1. 继续使用 OAuth2PasswordRequestForm：
   - 它包含类型为字符串列表的 scopes 属性。
   - scopes 属性中包含要在请求里接收的每个作用域。

2. 返回的 JWT 令牌中将包含作用域。

【危险】

1. 为了简明起见，本例把接收的作用域直接添加到了令牌里。

2. 但在实际开发中，为了安全，应该只把作用域添加到确实需要作用域的用户，或预定义的用户。
"""


@app.post("/token")
async def login_for_access_token(
        form_data: OAuth2PasswordRequestForm = Depends(),
) -> Token:
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username, "scopes": form_data.scopes},
        expires_delta=access_token_expires,
    )
    return Token(access_token=access_token, token_type="bearer")


# 获取当前用户信息
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user


# 获取当前用户的项目
@app.get("/users/me/items/")
async def read_own_items(
        current_user: User = Security(get_current_active_user, scopes=["items"]),
):
    return [{"item_id": "Foo", "owner": current_user.username}]


# 获取系统状态
@app.get("/status/")
async def read_system_status(current_user: User = Depends(get_current_user)):
    return {"status": "ok"}


"""
依赖项树与作用域

1. 查看依赖项树与作用域：
   - get_current_active_user 依赖项包含子依赖项 get_current_user，并在 get_current_active_user 中声明了作用域 "me" 包含所需作用域列表，在 security_scopes.scopes 中传递给 get_current_user。
   - 路径操作自身也声明了作用域 "items"，这也是 security_scopes.scopes 列表传递给 get_current_user 的。

2. 依赖项与作用域的层级架构：
   - 路径操作 read_own_items 包含：
     - 依赖项所需的作用域 ["items"]：
       - get_current_active_user:
         - 依赖项函数 get_current_active_user 包含：
           - 所需的作用域 "me" 包含依赖项：
             - get_current_user:
               - 依赖项函数 get_current_user 包含：
                 - 没有作用域需求其自身
                 - 依赖项使用 oauth2_scheme
   - security_scopes 参数的类型是 SecurityScopes：
     - security_scopes 参数的属性 scopes 是包含上述声明的所有作用域的列表，因此：
       - security_scopes.scopes 包含用于路径操作的 ["me", "items"]
       - security_scopes.scopes 包含路径操作 read_users_me 的 ["me"]，因为它在依赖项里被声明
       - security_scopes.scopes 包含用于路径操作 read_system_status 的 []（空列表），并且它的依赖项 get_current_user 也没有声明任何 scope

3. 提示：
   - get_current_user 检查每个路径操作时可以使用不同的 scopes 列表。
   - 所有这些都依赖于在每个路径操作和指定路径操作的依赖树中的每个依赖项。

SecurityScopes 的更多细节

1. 使用 SecurityScopes 的位置：
   - 您可以任何位置或多个位置使用 SecurityScopes，不一定非得在根依赖项中使用。
   - 它总是在当前 Security 依赖项中和所有依赖因子对于特定路径操作和特定依赖树中安全作用域。

2. 验证所需作用域的令牌：
   - 因为 SecurityScopes 包含所有由依赖项声明的作用域，可以在核心依赖函数中用它验证所需作用域的令牌，然后再在不同的路径操作中声明不同作用域需求。
   - 它们会为每个路径操作进行单独检查。

查看文档

1. 打开 API 文档，进行身份验证，并指定要授权的作用域：
   - 没有选择任何作用域，也可以进行身份验证，但访问 /users/me 或 /users/me/items 时，会显示没有足够的权限。但仍可以访问 /status/。
   - 如果选择了作用域 me，但没有选择作用域 items，则可以访问 /users/me/，但不能访问 /users/me/items。

2. 通过用户提供的令牌使用第三方应用访问：
   - 这就是通过用户提供的令牌使用第三方应用访问这些路径操作时会发生的情况，具体怎样取决于用户授予第三方应用的权限。

关于第三方集成

1. 使用 OAuth2 密码流：
   - 这种方式适用于登录我们自己的应用，最好使用我们自己的前端。
   - 因为我们能控制自己的前端应用，可以信任它接收 username 与 password。

2. 构建连接其它应用的 OAuth2 应用：
   - 如果构建的是连接其它应用的 OAuth2 应用，比如具有与脸书、谷歌、GitHub 相同功能的第三方身份验证应用。那您就应该使用其它安全流。
   - 最常用的是隐式流。
   - 最安全的是代码流，但实现起来更复杂，而且需要更多步骤。因为它更复杂，很多第三方身份验证应用最终建议使用隐式流。

3. 每个身份验证应用的命名方式：
   - 每个身份验证应用都会采用不同方式会命名流，以便融合入自己的品牌。
   - 但归根结底，它们使用的都是 OAuth2 标准。
   - FastAPI 的 fastapi.security.oauth2 里包含了所有 OAuth2 身份验证流工具。

装饰器 dependencies 中的 Security

1. 使用 Depends 列表：
   - 您可以在装饰器的 dependencies 参数中定义 Depends 列表（详见路径操作装饰器依赖项），也可以把 scopes 与 Security 一起使用。
"""
